Repository Public MIT
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { Markdown } from "@/components/markdown";5import { DOC_SECTIONS, getDoc, listDocs } from "@/lib/content";67type P = { params: Promise<{ slug?: string[] }> };8const slugOf = (s?: string[]) => (s === undefined || s.length === 0 ? "index" : s.length === 1 ? s[0]! : "");910export async function generateMetadata({ params }: P): Promise<Metadata> {11 const doc = getDoc(slugOf((await params).slug));12 return doc ? { title: `${doc.title} — Docs`, description: doc.description } : {};13}1415export default async function Page({ params }: P) {16 const slug = slugOf((await params).slug);17 const doc = getDoc(slug);18 if (!doc) notFound();19 const docs = listDocs();20 const sections = [...DOC_SECTIONS, ...new Set(docs.map((d) => d.section).filter((s) => !DOC_SECTIONS.includes(s)))];21 const idx = docs.findIndex((d) => d.slug === slug);22 const prev = idx > 0 ? docs[idx - 1] : null;23 const next = idx >= 0 && idx < docs.length - 1 ? docs[idx + 1] : null;24 const href = (s: string) => (s === "index" ? "/docs" : `/docs/${s}`);25 const nav = (26 <ul className="space-y-6">27 {sections.map((sec) => {28 const items = docs.filter((d) => d.section === sec);29 if (!items.length) return null;30 return (31 <li key={sec}>32 <p className="eyebrow mb-2">{sec}</p>33 <ul className="space-y-0.5 border-l border-line">34 {items.map((d) => (35 <li key={d.slug}>36 <Link href={href(d.slug)} aria-current={d.slug === slug ? "page" : undefined} className={`-ml-px block border-l py-1.5 pl-3 text-sm ${d.slug === slug ? "border-accent text-text" : "border-transparent text-muted hover:text-text"}`}>37 {d.title}38 </Link>39 </li>40 ))}41 </ul>42 </li>43 );44 })}45 <li>46 <p className="eyebrow mb-2">Machine-readable</p>47 <a href="/api/v1/openapi.json" className="block py-1.5 text-sm text-muted hover:text-text">OpenAPI 3.1 document</a>48 </li>49 </ul>50 );51 return (52 <div className="container-page grid gap-10 pb-10 pt-10 lg:grid-cols-[14rem_minmax(0,1fr)]">53 <details className="card p-4 lg:hidden">54 <summary className="cursor-pointer text-sm font-medium">Documentation menu</summary>55 <nav aria-label="Documentation (mobile)" className="mt-4">{nav}</nav>56 </details>57 <nav aria-label="Documentation" className="hidden lg:sticky lg:top-20 lg:block lg:max-h-[calc(100dvh-6rem)] lg:self-start lg:overflow-y-auto lg:pr-2">{nav}</nav>58 <article className="min-w-0 max-w-3xl">59 <p className="eyebrow mb-3">{doc.section}</p>60 <h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">{doc.title}</h1>61 {doc.description && <p className="mb-8 mt-3 text-lg text-muted">{doc.description}</p>}62 <Markdown source={doc.body.replace(/^\s*#\s+.*\n/, "")} trusted />63 <nav aria-label="Previous and next" className="mt-12 flex justify-between gap-4 border-t border-line pt-6 text-sm">64 {prev ? <Link className="link" href={href(prev.slug)}>← {prev.title}</Link> : <span />}65 {next ? <Link className="link text-right" href={href(next.slug)}>{next.title} →</Link> : <span />}66 </nav>67 </article>68 </div>69 );70}71